/*
 * Copyright (c) 2007, TopCoder, Inc. All rights reserved.
 */
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.Windows.Forms;
using TopCoder.Util.Calendar.Entry;

namespace TopCoder.WinForms.Controls.CalendarView.Views
{
    /// <summary>
    /// <para>This class draws the calendar data for a single month to the drawing panel. This class handles one
    /// month at a time, so the next and previous buttons only move forward and back one month at a time.</para>
    /// </summary>
    /// <remarks>This class itself is mutable, as it inherits the mutability of its parent class.</remarks>
    /// <author>Ghostar</author>
    /// <author>yext</author>
    /// <author>argolite</author>
    /// <author>alex.dolin</author>
    /// <version>1.1</version>
    /// <copyright>Copyright (c) 2007, TopCoder, Inc. All rights reserved.</copyright>
    public class MonthlyCalendarView : AbstractCalendarView
    {
        /// <summary>
        /// Represents the left padding in pixels.
        /// </summary>
        internal const int PADDING_LEFT = 10;

        /// <summary>
        /// Represents the right padding in pixels.
        /// </summary>
        internal const int PADDING_RIGHT = 10;

        /// <summary>
        /// Represents the top padding in pixels.
        /// </summary>
        internal const int PADDING_TOP = 5;

        /// <summary>
        /// Represents the bottom padding in pixels.
        /// </summary>
        internal const int PADDING_BOTTOM = 5;

        /// <summary>
        /// Represents the format string for the date value.
        /// </summary>
        internal const string DATE_FORMAT = "%d";

        /// <summary>
        /// The minimal width of each column.
        /// </summary>
        private const int MIN_COLUMN_WIDTH = 50;

        /// <summary>
        /// The minimal height of each row.
        /// </summary>
        private const int MIN_ROW_HEIGHT = 20;

        /// <summary>
        /// The minimal number of placeholders for each day.
        /// </summary>
        private const int MIN_ENTRIES_COUNT = 3;

        /// <summary>
        /// The height of the header row.
        /// </summary>
        private const int HEADER_HEIGHT = 40;

        /// <summary>
        /// The top margin of the header text.
        /// </summary>
        private const int HEADER_PADDING_TOP = 10;

        /// <summary>
        /// Represents the brush used to draw items.
        /// </summary>
        internal readonly static Brush itemBrush = new SolidBrush(Color.Black);

        /// <summary>
        /// Represents the font of the header cells.
        /// </summary>
        internal readonly static Font cellHeaderFont = new Font("Arial", 10);

        /// <summary>
        /// Represents the font of the item cells.
        /// </summary>
        internal readonly static Font itemFont = new Font("Arial", 10);

        /// <summary>
        /// The font for the header text.
        /// </summary>
        private readonly static Font headerFont = new Font("Arial", 10, FontStyle.Bold);

        /// <summary>
        /// The brush settings for the background of the header.
        /// </summary>
        private readonly static Brush headerBackBrush = new SolidBrush(Color.LightGray);

        /// <summary>
        /// The brush settings for the text of the header.
        /// </summary>
        private readonly static Brush headerBrush = new SolidBrush(Color.Gray);

        /// <summary>
        /// The pen settings for the border of the cells.
        /// </summary>
        internal readonly static Pen borderPen = new Pen(Color.LightGray);

        /// <summary>
        /// The brush settings for the background of the cells.
        /// </summary>
        internal readonly static Brush normalBackBrush = new SolidBrush(Color.WhiteSmoke);

        /// <summary>
        /// The brush settings for the background of the cell for today.
        /// </summary>
        internal readonly static Brush todayBackBrush = new SolidBrush(Color.LemonChiffon);

        /// <summary>
        /// The brush settings for the background of the cell in other months.
        /// </summary>
        internal readonly static Brush otherMonthBackBrush = new SolidBrush(Color.Gainsboro);

        /// <summary>
        /// The brush settings for the text of the date.
        /// </summary>
        internal readonly static Brush dateTextBrush = new SolidBrush(Color.Black);

        /// <summary>
        /// <para>Represents the start date of the current view.</para>
        /// </summary>
        private DateTime startDate;

        /// <summary>
        /// <para>Represents the calendar entries sorted by date.</para>
        /// </summary>
        private IDictionary<DateTime, IList<CalendarEntry>> sortedEntries;

        /// <summary>
        /// Represents the name of the day view, to be used by the navigation panels.
        /// May be null, which would result in navigation not transfering to new view. Will not be empty.
        /// Managed by DayViewType property.
        /// </summary>
        private string dayViewType;

        /// <summary>
        /// Represents the name of the week view, to be used by the navigation panels.
        /// May be null, which would result in navigation not transfering to new view. Will not be empty.
        /// Managed by WeekViewType property.
        /// </summary>
        private string weekViewType;

        /// <summary>
        /// Indicates whether the data has changed since last view update.
        /// </summary>
        private bool dataChanged = false;

        /// <summary>
        /// <para>Use the specified date to filter the calendar entries.</para>
        /// </summary>
        private void FilterData()
        {
            startDate = DateOfInterest.Date - new TimeSpan(DateOfInterest.Day - 1, 0, 0, 0);

            // Set the title text.
            TitleLabelText = startDate.ToString("y");

            // Calculate the end date.
            DateTime endDate = startDate.AddMonths(1).AddDays(-1);

            // Select entries in that month.
            CurrentData = Helper.SelectEntriesByDateRange(AllData, startDate, endDate);

            // Sort the entries by date.
            sortedEntries = Helper.GroupEntriesByDate(CurrentData);

            // Recreate all panels because current data has changed
            dataChanged = true;
            DrawingPanel.Invalidate();
        }

        /// <summary>
        /// Creates child panels.
        /// </summary>
        private void CreatePanels()
        {
            DrawingPanel.SuspendLayout();

            Helper.ReleaseChildControls(DrawingPanel);

            // Create the header:
            Panel weekPanel = new NavigationPanel(this, WeekViewType, startDate);
            weekPanel.Name = "WeekPanel";
            weekPanel.Parent = DrawingPanel;
            weekPanel.Paint += new PaintEventHandler(WeekPanelPaint);

            // Create the day panels:
            DateTime viewDate = startDate.AddDays(-(int)startDate.DayOfWeek);
            for (int i = 0; i < 6; i++)
            {
                for (int j = 0; j < 7; j++)
                {
                    Panel dayPanel = new NavigationPanel(this, DayViewType, viewDate);
                    dayPanel.Name = viewDate.ToShortDateString();
                    dayPanel.Parent = DrawingPanel;
                    dayPanel.Paint += new DayPaintHandler(viewDate, startDate).OnPaint;

                    if (sortedEntries.ContainsKey(viewDate))
                    {
                        foreach (CalendarEntry entry in sortedEntries[viewDate])
                        {
                            Panel entryPanel =
                                new EntryDescriptionPopupPanel(entry, false, this, DayViewType, viewDate);
                            entryPanel.Parent = dayPanel;
                            entryPanel.Paint +=
                                new EntryPaintHandler(entry, this).OnPaint;
                        }
                    }

                    viewDate = viewDate.AddDays(1);
                }
            }

            DrawingPanel.ResumeLayout();
        }

        /// <summary>
        /// Updates layout of the child panels.
        /// </summary>
        private void UpdatePanelsLayout()
        {
            if(dataChanged)
            {
                CreatePanels();
            }
            float measuredColumnWidth = MIN_COLUMN_WIDTH;
            float measuredTotalWidth;
            float measuredRowHeight = MIN_ROW_HEIGHT;
            int maxEntriesCount = MIN_ENTRIES_COUNT;
            IDictionary<CalendarEntry, float> entryWidth =
                new Dictionary<CalendarEntry, float>(CurrentData.Count);

            #region Measurements:

            using (Graphics g = DrawingPanel.CreateGraphics())
            {
                // Check for the header row:
                for (int i = 0; i < 7; i++)
                {
                    SizeF size = g.MeasureString(((DayOfWeek)i).ToString(), headerFont);
                    measuredColumnWidth = Math.Max(measuredColumnWidth, size.Width + PADDING_LEFT + PADDING_RIGHT);
                }

                foreach (IList<CalendarEntry> entries in sortedEntries.Values)
                {
                    maxEntriesCount = Math.Max(maxEntriesCount, entries.Count);

                    // The short date cell:
                    SizeF size = g.MeasureString(entries[0].Date.Day.ToString(), itemFont);
                    measuredColumnWidth = Math.Max(measuredColumnWidth, size.Width + PADDING_LEFT + PADDING_RIGHT);
                    measuredRowHeight = Math.Max(measuredRowHeight, size.Height + PADDING_TOP + PADDING_BOTTOM);

                    // The entries:
                    foreach (CalendarEntry entry in entries)
                    {
                        size = g.MeasureString(entry.Description, itemFont);
                        measuredColumnWidth = Math.Max(measuredColumnWidth, size.Width + PADDING_LEFT + PADDING_RIGHT);
                        measuredRowHeight = Math.Max(measuredRowHeight, size.Height + PADDING_TOP + PADDING_BOTTOM);
                        entryWidth[entry] = size.Width + PADDING_LEFT + PADDING_RIGHT;
                    }
                }

                measuredTotalWidth = measuredColumnWidth * 7;
                float measuredTotalHeight = HEADER_HEIGHT + measuredRowHeight * (maxEntriesCount + 1) * 6;

                // Adjust the size of DrawingPanel.
                Helper.AdjustPanelSize(HolderPanel, DrawingPanel,
                                       (int)Math.Ceiling(measuredTotalWidth), (int)Math.Ceiling(measuredTotalHeight),
                                       ColumnWidthRendering);

                measuredTotalWidth = DrawingPanel.Width;
                measuredColumnWidth = measuredTotalWidth / 7;
            }

            float cellHeight = measuredRowHeight * (maxEntriesCount + 1);

            #endregion

            #region Draw the panel:

            float y = 0;

            // Draw the header:
            Panel weekPanel = (Panel)DrawingPanel.Controls["WeekPanel"];
            Helper.UpdatePanel(weekPanel, 0, 0, (int)Math.Ceiling(measuredTotalWidth), HEADER_HEIGHT);

            y += HEADER_HEIGHT;

            DateTime viewDate = startDate.AddDays(-(int)startDate.DayOfWeek);
            for (int i = 0; i < 6; i++)
            {
                float x = 0;
                for (int j = 0; j < 7; j++)
                {
                    UpdateCellLayout(viewDate, x, y, measuredColumnWidth, cellHeight, measuredRowHeight, entryWidth);
                    x += measuredColumnWidth;
                    viewDate = viewDate.AddDays(1);
                }
                y += cellHeight;
            }
            #endregion

            dataChanged = false;
        }

        /// <summary>
        /// Represents the date around which this view should be built. The resulting view must inlcude this date.
        /// By default, the current date is used.
        /// </summary>
        /// <value>The date around which this view should be built.</value>
        public override DateTime DateOfInterest
        {
            set
            {
                base.DateOfInterest = value;
                FilterData();
            }
        }

        /// <summary>
        /// <para>This method draws the drawing panel with the current calendar entries.</para>
        /// </summary>
        /// <param name="sender">The sender of the event.</param>
        /// <param name="e">The event, containing the graphic.</param>
        protected override void OnDrawingPanelPaint(object sender, PaintEventArgs e)
        {
            UpdatePanelsLayout();
        }

        /// <summary>
        /// <para>This private method draws a cell for a day in the drawing panel.</para>
        /// </summary>
        /// <param name="date">The date of the cell.</param>
        /// <param name="x">The x position of the cell.</param>
        /// <param name="y">The y position of the cell.</param>
        /// <param name="width">The width of the cell.</param>
        /// <param name="height">The height of the cell.</param>
        /// <param name="rowHeight">The height of each row in the cell.</param>
        /// <param name="entryWidth">The actual width of each entry without truncating.</param>
        private void UpdateCellLayout(DateTime date, float x, float y, float width, float height, float rowHeight,
            IDictionary<CalendarEntry, float> entryWidth)
        {
            Panel dayPanel = (Panel)DrawingPanel.Controls[date.ToShortDateString()];
            Helper.UpdatePanel(dayPanel, (int)Math.Ceiling(x), (int)Math.Ceiling(y),
                (int)Math.Floor(width), (int)Math.Floor(height));

            if(sortedEntries.ContainsKey(date))
            {
                y = rowHeight;
                int entryIndex = 0;
                foreach (CalendarEntry entry in sortedEntries[date])
                {
                    CalendarEntryPopupPanel entryPanel = (CalendarEntryPopupPanel)dayPanel.Controls[entryIndex];
                    Helper.UpdatePanel(entryPanel, 1, (int)Math.Ceiling(y + 1),
                        (int)Math.Ceiling(width - 2), (int)Math.Ceiling(rowHeight - 2));
                    entryPanel.Truncated = entryWidth[entry] > entryPanel.Width + 1;

                    y += rowHeight;
                    entryIndex++;
                }
            }
        }

        /// <summary>
        /// Creates a new instance of this class.
        /// </summary>
        /// <param name="parent">The parent calendar view control.</param>
        /// <exception cref="ArgumentNullException">If <paramref name="parent"/> is null.</exception>
        public MonthlyCalendarView(CalendarViewControl parent)
            : base(parent)
        {
            FilterData();
        }

        /// <summary>
        /// Represents the name of the day view, to be used by the navigation panels.
        /// May be null, which would result in navigation not transfering to new view.
        /// Empty is set to null.
        /// </summary>
        /// <value>The name of the day view, to be used by the navigation panels.</value>
        public string DayViewType
        {
            get
            {
                return dayViewType;
            }
            set
            {
                dayViewType = Helper.GetNullIfEmpty(value);
            }
        }

        /// <summary>
        /// Represents the name of the week view, to be used by the navigation panels.
        /// May be null, which would result in navigation not transfering to new view.
        /// Empty is set to null.
        /// </summary>
        /// <value>The name of the week view, to be used by the navigation panels.</value>
        public string WeekViewType
        {
            get
            {
                return weekViewType;
            }
            set
            {
                weekViewType = Helper.GetNullIfEmpty(value);
            }
        }

        /// <summary>
        /// <para>This method is called when the "Next" button is pressed on the header of the view.
        /// This method should get all calendar data for the month that is after the current month being shown.
        /// </para>
        /// </summary>
        /// <param name="sender">The next button that was pressed.</param>
        /// <param name="e">The event args.</param>
        /// <returns>void</returns>
        protected override void OnNextButtonClick(object sender, EventArgs e)
        {
            DateOfInterest = startDate.AddMonths(1);
        }

        /// <summary>
        /// <para>This method is called when the "Previous" button is pressed on the header of the view.
        /// This method should get all calendar data for the month that is before the current month being shown.
        /// </para>
        /// </summary>
        /// <param name="sender">The previous button that was clicked.</param>
        /// <param name="e">The event args.</param>
        protected override void OnPreviousButtonClick(object sender, EventArgs e)
        {
            DateOfInterest = startDate.AddMonths(-1);
        }

        /// <summary>
        /// Processes <see cref="Control.Paint"/> event for the week header panel.
        /// </summary>
        /// <param name="sender">The sender of the event.</param>
        /// <param name="e">The event, containing the graphic.</param>
        private static void WeekPanelPaint(object sender, PaintEventArgs e)
        {
            Panel weekPanel = (Panel)sender;
            Graphics g = e.Graphics;
            float x = 0;
            float columnWidth = weekPanel.Width / 7;
            // The background color:
            g.FillRectangle(headerBackBrush, weekPanel.ClientRectangle);

            for (int i = 0; i < 7; i++)
            {
                // The text:
                g.DrawString(DateTimeFormatInfo.CurrentInfo.DayNames[i], headerFont, headerBrush,
                              x + PADDING_LEFT, HEADER_PADDING_TOP);
                x += columnWidth;
            }
        }

        /// <summary>
        /// Private helper class to draw day cell.
        /// </summary>
        /// <remarks>Thread Safety: This class is immutable and thread-safe.</remarks>
        private class DayPaintHandler
        {
            /// <summary>
            /// The date of this cell.
            /// </summary>
            private readonly DateTime date;

            /// <summary>
            /// The first day of the month this cell belongs to.
            /// The date of this cell may be from a different month (previous or next).
            /// </summary>
            private readonly DateTime startDate;

            /// <summary>
            /// Creates a new instance of the class.
            /// </summary>
            /// <param name="date">The date of this cell.</param>
            /// <param name="startDate">The first day of the month this cell belongs to.</param>
            public DayPaintHandler(DateTime date, DateTime startDate)
            {
                this.date = date;
                this.startDate = startDate;
            }

            /// <summary>
            /// Processes <see cref="Control.Paint"/> event for one day panel.
            /// </summary>
            /// <param name="sender">The sender of the event.</param>
            /// <param name="e">The event, containing the graphic.</param>
            public void OnPaint(object sender, PaintEventArgs e)
            {
                Panel dayPanel = (Panel)sender;
                Graphics g = e.Graphics;
                float y = 0;
                // Draw the background:
                Brush backColorBrush = normalBackBrush;
                if (date.Year != startDate.Year || date.Month != startDate.Month)
                {
                    backColorBrush = otherMonthBackBrush;
                }
                else if (date == DateTime.Now.Date)
                {
                    backColorBrush = todayBackBrush;
                }
                g.FillRectangle(backColorBrush, dayPanel.ClientRectangle);

                // Draw the border:
                g.DrawRectangle(borderPen, dayPanel.ClientRectangle);

                // The header date:
                g.DrawString(date.ToString(DATE_FORMAT), cellHeaderFont,
                             dateTextBrush, PADDING_LEFT,
                             y + PADDING_TOP);
            }
        }

        /// <summary>
        /// Private helper class to draw one entry.
        /// </summary>
        /// <remarks>Thread Safety: This class is immutable and thread-safe.</remarks>
        private class EntryPaintHandler
        {
            /// <summary>
            /// The calendar entry instance which holds data to display.
            /// </summary>
            private readonly CalendarEntry entry;

            /// <summary>
            /// The calendar view to provide color for entry type.
            /// </summary>
            private readonly AbstractCalendarView view;

            /// <summary>
            /// Creates a new instance of the class.
            /// </summary>
            /// <param name="entry">The calendar entry instance which holds data to display.</param>
            /// <param name="view">The calendar view to provide color for entry type.</param>
            public EntryPaintHandler(CalendarEntry entry, AbstractCalendarView view)
            {
                this.entry = entry;
                this.view = view;
            }

            /// <summary>
            /// Processes <see cref="Control.Paint"/> event for one entry panel.
            /// </summary>
            /// <param name="sender">The sender of the event.</param>
            /// <param name="e">The event, containing the graphic.</param>
            public void OnPaint(object sender, PaintEventArgs e)
            {
                Panel entryPanel = (Panel)sender;
                Graphics g = e.Graphics;

                // Draw the background for the entry type:
                Color colorForType = view.GetColor(entry.Type);
                if (colorForType != Color.Transparent)
                {
                    g.FillRectangle(new SolidBrush(colorForType), entryPanel.ClientRectangle);
                }

                // Draw the text:
                RectangleF cellRect = new RectangleF(PADDING_LEFT - 1, PADDING_TOP - 1,
                                                     entryPanel.Width - PADDING_LEFT - PADDING_RIGHT + 1,
                                                     entryPanel.Height - PADDING_TOP - PADDING_BOTTOM + 1);

                g.DrawString(entry.FullName, itemFont, itemBrush, cellRect, Helper.StringTrimFormat);
            }
        }
    }
}
